-
-
Notifications
You must be signed in to change notification settings - Fork 924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Revised Component types (children, ref) #877
Conversation
* Rename past `Component` type to `ComponentWithChildren`. Motivation: Most components don't actually support children. This causes a type error upon accidental passing of children / forces you to think about which components support children. * Added second parameter to `ComponentWithChildren` to make it easier to give a custom type for `children` (but still defaults to useful `JSX.Element`). * New `Component` type does not have automatic `children` property (like React's preferred `VoidFunctionalComponent`), offering another natural way to type `props.children`: `Component<{children: JSX.Element}>`. * `props` argument in both `Component` and `ComponentWithChildren` automatically cast to `readonly` (shallow one level only), to avoid accidental assignment to `props.foo` (usually a getter) while still allowing passing mutables in props. Add `Props<T>` helper for this transformation. * Add @lxsmnsyc's `Ref<T>` type so it's easy to type `props.ref`: `Component<{ref: Ref<Element>}>`. <solidjs#778 (comment)> Fixes solidjs#778.
Pull Request Test Coverage Report for Build 2260557767
💛 - Coveralls |
This Playground link shows why this is an issue well: https://playground.solidjs.com/?hash=865026612&version=1.3.9 There are two main problems:
|
Based on further discussion on #typescript, I've changed this in two ways:
One motivation for this change is that it would not play well with existing explicitly typed function MyComponent(props: {name: string}): JSX.Element { ... } This type is not consistent with Another motivation for this change is that <A {...{ set a(){} }} /> |
In my opinion this is a stretch. And while obviously it isn't stoppable I wouldn't give it any consideration or motivation here. 99.999% case shallow readonly is what you want. I can't speak to other implications so if it makes sense not to type that way I'm all for it. But the setter shouldn't be a consideration. |
@high1 has proposed removing |
I still think that component should be the default use case, which includes the composability of having children; like with HTML you get void elements which have no children. So I would prefer to have VoidComponents and VoidProps for this special case. |
In my opinion, since children are something that not all components will need (really, the majority of them, mainly wrapper components that apply some styling or have their own local state), it makes more sense to write the names in an additive way, which would mirror the natural usage, such as |
If My opinion is that with these two types, it may be confusing to have |
If you know JSX, chances are you are already familiar with the react types. And if not, we're not exactly clashing with preconceived expectations, are we? |
FWIW, I know React well, but never used React with TypeScript. I started using TypeScript at roughly the same time I started using Solid. So it's all new to me, and I find React's types pretty confusing (and React seems to regret them). So I'm not convinced "same as React" is the right goal... By contrast, I'd argue that there's a natural notion of But I also originally proposed adding |
tbf I've never used the built-in types since I can just define my own props interface. I'm down with the |
@lxsmnsyc I suppose another option is to just deprecate |
@edemaine I don't think we should deprecate it. The main question is - should the Component type receive children by default, as a number of people expects, and I would say that's what the main purpose of a Component is, and if that's the case, VoidComponent type makes sense as a type that does not receive children. I don't think that there should be any assumptions which type is used more often. I also see the argument which comes from @otonashixav, that by unifying everything in one Component type which would require the user to be explicit with children declaration, and TBH, I'm starting to be more and more in favor of the latter, but I'm OK with any of these two. I don't have a very strong case against ComponentWithChildren, I just don't like the name and I feel that we would be more in line with what users are expecting by using Component and VoidComponent, favored by @atk, or be more on the edge, and simplify the typings by using one Component type which requires explicit children. |
Based on more yet more discussion on #typescript with @atk and @otonashixav, I revised further to this plan:
I also tried to clean up some use of types in the code, and add some type exports to server code (I assume this is necessary to repeat here?). I'm not sure I found all places where these types could be used in the code, but I think it's an improvement. |
My bad, when I said that I only quickly tested in solid's playground (not a good place to test typescript) and assumed it would work the same as function lazy<T>(
fn: () => Promise<{ default: Component<T> }>
): Component<T> & { preload: () => Promise<{ default: Component<T> }> should work. |
Thanks @otonashixav! For backward compatibility, I changed the definition of |
Something just to add that I've seen throughout my React/Typescript years is the need to specify the Type of children. Will something like the |
Yes, that's exactly the intent. |
When you invoke a component using JSX (
|
@edemaine @otonashixav Is VoidComponent needed at all, then? All components would be void by default - and that seems to deprecate VoidComponent, doesn't it? I see no reason to have it as a separate type. |
While you cannot call either directly with children, the former is still allowed to be assigned to a type that may accept children, whereas the former cannot since it never accepts children. // note: this doesn't error in the playground because of strict: false
let MyComponent!: Component
let MyVoidComponent!: VoidComponent
let A: ParentComponent = MyComponent;
let B: ParentComponent = MyVoidComponent;
/* ^ Type 'VoidComponent<{}>' is not assignable to type 'ParentComponent<{}>'.
Types of parameters 'props' and 'props' are incompatible.
Type '{ children?: Element; }' is not assignable to type '{ children?: undefined; }'.
Types of property 'children' are incompatible.
Type 'Element' is not assignable to type 'undefined'.
Type 'null' is not assignable to type 'undefined'.(2322) */ Practically, this affects functions which accept a component e.g.: https://playground.solidjs.com/?hash=-853165744&version=1.3.13 |
@otonashixav I've looked at the playground, and it seems weird that the Dynamic accepts children property, when a plain component is referenced. Isn't the problem maybe that Dynamic uses current definitions which have implicit children? |
That's probably why that specific example works in solid's playground. See https://tsplay.dev/w1yAKw instead. |
The children will not be silently accepted on the Component - if not explicitly defined. I don't see a lot of value in explicitly forbidding them, when they first have to be opt-in. On the other hand, maybe having a convenience type is not bad... |
Inspired by Otonashi's post, one context I could see using function wrapper(component: VoidComponent<any>) {
// use component without giving it children
} This seems like a pretty special case though, so I'm not sure we need to provide a type helper for it. I'm certainly happy to remove it if there's consensus; it does feel redundant. But as @atk suggested it, maybe he can comment? |
It was meant as a convenience type for library developers to make sure users were not confused by components that don't render children. Since types usually don't influence the bundle size of resulting pages and apps, it shouldn't be an issue to provide them. |
@atk @edemaine @otonashixav My main concern here is the user viewpoint - now we have a Component type which doesn't have implicit children, VoidComponent which explicitly forbids them, ParentComponent which brings back implicit children. How should a user type his component which doesn't accept children? Should he type it with Component or a VoidComponent - and is there a real difference in the most prevalent usage case? That's the main case for having one primitive type - but people could also find that too limiting and constrained. |
We need to document these types (JSDoc; especially VoidComponent and ParentComponent need to be mentioned in Component's description). They could basically build VoidComponent themselves, but using a dedicated type which reuses the pattern of Void Elements in the HTML living standard is much more obvious and simple. |
Not explicitly typing children as never only means losing a type error in a single unlikely scenario. For the most common case there isn't a difference. Users are free to choose whatever suits them. They can define the types themselves without using any of the helpers, use only Component, or use all of the helpers. The extra types are named such that it is clear they are a special subset of components with extra semantic value, so I don't see it being difficult to decide which to choose. That said, if I had to argue for their removal:
For both of these good documentation and JSDoc will help. |
I see that React has deprecated VoidFunctionComponent - and while I'm OK with keeping it here, adding these additional types only to the documentation would also serve the purpose, wouldn't it? The user could just copy the definitions for these if he feels he needs them, and we would avoid adding utility types to the core - types which are not used anywhere inside the codebase itself. |
As I already remarked, it's a question of DX: if we already provide the type and the documentation, we save the developers some work while at the same time helping to make their types be as sound and useful as possible. These types don't increase our bundle size noticably and will not even appear in the resulting app bundles, so what's not to like? I also believe the react type was deprecated, because it lacked sufficient documentation, which led to confusion they sought to clean up by doing so. We can avoid the confusion to begin with, so that shouldn't be an issue. |
@@ -1063,7 +1064,7 @@ export interface Context<T> { | |||
* ```typescript | |||
* interface Context<T> { | |||
* id: symbol; | |||
* Provider: (props: { value: T; children: any }) => any; | |||
* Provider: ParentComponent<{ value: T }>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably forgot to change this to FlowComponent
From a "compatible naming" perspective, it does bother me a bit that |
I'm all on the side of adding only what's needed, maybe removing ParentComponent also, and having children optional in FlowComponent would be the best course? |
Oops, didn't mean to open the can of worms that far. 😄 I feel like On the other hand, I think |
I would agree to this sentiment if we were talking about API surface – however this is about types, which are more documentation than code and in documentation, you should cover everything that is helpful to most people. |
Thank you all for all your effort to get this one across the finish line. |
* resource and reactive updates, fix #919 * stores: top level arrays, prevent obj overnotify, fixes #960 batching mutable * fix #940 undefined ssr, fix #955 undefined className, fix #958 undefined spread * changelog wip * fix: fix the signal setter type when used with generics (#910) This makes it easier to create generic wrappers around `createSignal` e.g. ```ts function createGenericSignal<T>(): Signal<T | undefined> { const [generic, setGeneric] = createSignal<T>(); const customSet: Setter<T | undefined> = (v?) => setGeneric(v); return [generic, (v?) => setGeneric(v)]; } function createInitializedSignal<T>(init: T): Signal<T> { const [generic, setGeneric] = createSignal<T>(init); const customSet: Setter<T> = (v?) => setGeneric(v); return [generic, (v?) => setGeneric(v)]; } ``` where previously such the implementation would not be assignable to `Setter`, and would need to be cast. Unexpectedly this also makes this more specific: ```ts const [s, set] = createSignal<string>(); // <-- signal contains undefined // before: const v = set(() => "literal"); // ^? - string // after: const v = set(() => "literal"); // ^? - "literal" ``` * refactor: improve splitProps type for arbitrary rest args length (#930) - Allows the return type of splitProps to be correct for 1 or more `...keys` passed - Removes overloads for `splitProps` as they are no longer necessary - Types each array passed as rest args as `readonly` as they do not need to be mutable * v1.4.0-beta.0 * fix: spreading non-object props (#963) - `createComponent` passes an empty object instead of non-object props which are spread - `mergeProps` treats non-object sources as empty objects - added tests fixes #958 * Revised Component types (children, ref) (#877) * Revised Component types (children, readonly, ref) * Rename past `Component` type to `ComponentWithChildren`. Motivation: Most components don't actually support children. This causes a type error upon accidental passing of children / forces you to think about which components support children. * Added second parameter to `ComponentWithChildren` to make it easier to give a custom type for `children` (but still defaults to useful `JSX.Element`). * New `Component` type does not have automatic `children` property (like React's preferred `VoidFunctionalComponent`), offering another natural way to type `props.children`: `Component<{children: JSX.Element}>`. * `props` argument in both `Component` and `ComponentWithChildren` automatically cast to `readonly` (shallow one level only), to avoid accidental assignment to `props.foo` (usually a getter) while still allowing passing mutables in props. Add `Props<T>` helper for this transformation. * Add @lxsmnsyc's `Ref<T>` type so it's easy to type `props.ref`: `Component<{ref: Ref<Element>}>`. <#778 (comment)> Fixes #778. * Fix test * Remove Props helper and shallow Readonly for props * VoidComponent, ParentComponent, FlowComponent * Use Component<any> for generic component type * Add default types, Context.Provider require children Comments from Otonashi * Default parameter for PropsWithChildren * Restore missing <any> * Docs typo fix * Cleanup server code * JSDoc improvements * Improve FlowProps JSDoc * More FlowComponent JSDoc improvements Co-authored-by: Ryan Carniato <[email protected]> * refactor: simplify effect types (#913) - Simplified `createEffect` etc. such that they no longer use rest args - Added test cases for effect functions failing partial generic inference (not supported by typescript) - Removed extra params from effects' first overload * optimize: use jest fake timer to test suspense (#964) fix #761 * new store modifiers splice, modifyMutable, bug fixes * v1.4.0-beta.1 * refactor: remove extra generic from modifyMutable (#965) * fix splice length setting * remove unnecessary splice * feat: resource deferred streaming * v1.4.0-beta.2 * support standard JSX transform * fix #967 `onHydrated` * v1.4.0-beta.3 * Different types for standard transform * v1.4.0-beta.4 * fix solid hyperscript, startTransition cleanup * fix build * v1.4.0-beta.5 * fix synchronous resources * more changelog updates * fix batch consistency in stores * v1.4.0-beta.6 * typescript: correct on helper types (#959) * refactor: correct on helper types - Previous input type now includes undefined, which is its initial value - When defer is true, memos created with the on helper include undefined - Fixed return type becoming unknown when passing a function with a third parameter (the previous type) - Allow readonly arrays/tuples to be passed as deps - Disallow empty arrays from being passed as deps - Breaks backwards compatibility of the first generic; now it is the type of the inputs passed to the function instead of the type of the deps themselves: ```ts // before on<number>(() => 1, ...) // error on<() => number>(() => 1, ...) // ok on<[number]>([() => 1], ...) // error on<[() => number]>([() => 1], ...) // ok // now on<number>(() => 1, ...) // ok on<() => number>(() => 1, ...) // error on<() => number>(() => () => 1, ...) // ok on<[number]>([() => 1], ...) // ok on<[() => number]>([() => 1], ...) // error on<[() => number]>([() => () => 1], ...) // ok ``` * docs: update on helper jsdoc * fix: missing readonly in on helper types' `AccessorTuple` * refactor: allow any array in `AccessorTuple` - So that passing arrays works - Renamed to `AccessorArray` Co-authored-by: Ryan Carniato <[email protected]> Co-authored-by: Xavier Loh <[email protected]> Co-authored-by: Erik Demaine <[email protected]> Co-authored-by: WZT <[email protected]>
I'm quite a bit late on this, but since SolidJS doesn't seem to follow semantic versioning, it's worth noting the change from |
Yeah TypeScript has been very difficult to keep with semantic versioning. This is partly because I don't think in TS and it takes some time for understanding the use cases in practice. Also TypeScript has been making updates that significantly improve typing story. In general these have been deemed worthwhile because of their impact and we've tried to keep them to minor versions. |
Based on recent discussion on #typescript. This change summary is longer than the diff. 🙂
Rename past
Component
type toComponentWithChildren
ParentComponent
.Component
type toParentComponent
to avoid type errors) so should probably wait for Solid 1.4.Two-parameter
FlowComponent
requireschildren
and makes it easy to give a custom type forchildren
, as in many flow control components.New
Component
type does not have automaticchildren
property (like React's preferredVoidFunctionalComponent
), offering another natural way to typeprops.children
:New
VoidComponent
forces not havingchildren
prop, to make it extra clear how to type this.ParentProps
,FlowProps
, andVoidProps
type helpers for theprops
argument toParentComponent
,FlowComponent
, andVoidComponent
.Sadly none of these are great when you need to parameterize by a generic type, but still good starting points for people typing simple components.
Removed; see below.props
argument in bothComponent
andComponentWithChildren
automatically cast toreadonly
(shallow one level only), to avoid accidental assignment toprops.foo
(usually a getter) while still allowing passing mutables in props.AddProps<T>
helper for this transformation.Add @lxsmnsyc's
Ref<T>
type so it's easy to typeprops.ref
:Fixes Add type for PropsWithRef to define components that accept refs #778.
An alternative would be to leave
Component
as is, and rename the newComponent
type toVoidComponent
. This would preserve backward compatibility and match React. But see this discussion for good arguments why (in React)VoidComponent
is better than straightComponent
. And React 18 is changing the type ofFunctionalComponent
to what wasVoidFunctionalComponent
.